﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows;
using CannedHeater.Model;

namespace CannedHeater.ViewModel
{
    public class GraphViewModel : INotifyPropertyChanged
    {
        #region Locally used variables
        /// <summary>
        /// Whether or not the graph is being filled with data for the first time since creation of this viewmodel
        /// </summary>
        private bool _firstLoad = true;

        /// <summary>
        /// The estimated amount of datapoints to be shown in a graphseries (estimated due to tiny time differences in datapoints)
        /// </summary>
        private const int ESTIMATED_POINTS_TO_SHOW = 100;

        /// <summary>
        /// The possible ranges that the graph components will show.
        /// Ordered ascending
        /// </summary>
        private TimeSpan[] _ranges = { new TimeSpan(3,0,0),
                                       new TimeSpan(24,0,0),
                                       new TimeSpan(7,0,0,0),
                                       new TimeSpan(30,0,0,0)};
        #endregion

        #region Types
        /// <summary>
        /// The graph component a dataseries belongs to
        /// </summary>
        public enum GraphDataGroup
        {
            /// <summary>
            /// Temperature graph
            /// </summary>
            Temp,
            /// <summary>
            /// AirFlow graph
            /// </summary>
            AirFlow
        }
        #endregion

        #region Events
        public event PropertyChangedEventHandler PropertyChanged;
        #endregion

        #region Properties
        /// <summary>
        /// The DataFetcher to get the data from
        /// </summary>
        private DataFetcher _df;
        public DataFetcher DF
        {
            get { return _df; }
            set
            {
                _df = value;
                DF.DataFetchCompleted += new DataFetcher.DataFetchCompletedEventHandler(DF_DataFetchCompleted);
                ReloadGraphData();
            }
        }

        private GraphCollection _tempDataCollection = new GraphCollection();
        /// <summary>
        /// A collection of graphs to be shown in the temperature graph component
        /// </summary>
        public GraphCollection TempDataCollection
        {
            get { return _tempDataCollection; }
            private set { _tempDataCollection = value; }
        }

        private GraphCollection _airFlowDataCollection = new GraphCollection();
        /// <summary>
        /// A collection of graphs to be shown in the air flow graph component
        /// </summary>
        public GraphCollection AirFlowDataCollection
        {
            get { return _airFlowDataCollection; }
            private set { _airFlowDataCollection = value;  }
        }

        private string _tempUnit = "";
        /// <summary>
        /// The unit for temperatures (not used at this time)
        /// </summary>
        public string TempUnit
        {
            get { return _tempUnit;  }
            private set { _tempUnit = value;  }
        }

        private string _airFlowUnit = "";
        /// <summary>
        /// The unit for air flow (not used at this time)
        /// </summary>
        public string AirFlowUnit
        {
            get { return _airFlowUnit; }
            private set { _airFlowUnit = value; }
        }

        private TimeSpan _tempRange = TimeSpan.Zero;
        /// <summary>
        /// The range (time) for which temperature data should be shown
        /// </summary>
        public TimeSpan TempRange
        {
            get { return _tempRange; }
            set
            {
                _tempRange = value;
                RaisePropertyChanged("TempRange");
            }
        }

        private TimeSpan _airFlowRange = TimeSpan.Zero;
        /// <summary>
        /// The range (time) for which air flow data should be shown
        /// </summary>
        public TimeSpan AirFlowRange
        {
            get { return _airFlowRange; }
            set
            {
                _airFlowRange = value;
                RaisePropertyChanged("AirFlowRange");
            }
        }
        #endregion

        #region Constructors
        /// <summary>
        /// Constructs a new GraphViewModel
        /// </summary>
        public GraphViewModel()
        {
            // Use default ranges (smallest)
            TempRange = _ranges[0];
            AirFlowRange = _ranges[0];
        }
        #endregion

        /// <summary>
        /// Reloads the graphs (clear and fill with new data).
        /// Both reloaded at the same time (IMPROVEMENT suggestion: Separate)
        /// </summary>
        private void ReloadGraphData()
        {
            // On first load, just show the available data
            if (_firstLoad)
            {
                AddLoadedData();
                _firstLoad = false;
                return;
            }

            // Successive reloads

            // Clear the graphs
            _tempDataCollection.Clear();
            _airFlowDataCollection.Clear();

            // Request data
            bool res = false;
            // Use the biggest range (since both temperature and airflow data is loaded at the same time for now)
            if (_airFlowRange > _tempRange)
                res = DF.FetchDataIfUnavailable(_airFlowRange);
            else
                res = DF.FetchDataIfUnavailable(_tempRange);
            
            // If all data needed is already available, just add the loaded data to graph immediatly
            // Otherwise an event will be raised by DataFetcher (see DF_DataFetchCompleted)
            if ( res ) AddLoadedData();
        }

        /// <summary>
        /// Handles new data fetched by datafetcher
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void DF_DataFetchCompleted(object sender, DataFetcher.DataFetchCompletedEventArgs args)
        {
            AddLoadedData(); // Just add the data to the graph
        }

        /// <summary>
        /// Adds new data to graph (actually clears graph and reload all data to graphs - could be improved)
        /// </summary>
        private void AddLoadedData()
        {
            // Clear graphs
            if (_tempDataCollection.Count != 0) _tempDataCollection.Clear();
            if (_airFlowDataCollection.Count != 0) _airFlowDataCollection.Clear();

            // Filter datapoints (limit the amount of datapoints added to the graph for performance issues)
            // Add filtered data to graph, adding a title to the dataseries
            _tempDataCollection.Add(new GraphDataSeries(FilterDataPoints(_tempRange, DF.DH.IndoorTempData), "Indoor"));
            _tempDataCollection.Add(new GraphDataSeries(FilterDataPoints(_tempRange, DF.DH.OutdoorTempData), "Outdoor"));
            _tempDataCollection.Add(new GraphDataSeries(FilterDataPoints(_tempRange, DF.DH.SolarHeaterTempData), "Solar Heater"));

            _airFlowDataCollection.Add(new GraphDataSeries(FilterDataPoints(_airFlowRange, DF.DH.AirFlowData), "Air Flow"));
        }

        /// <summary>
        /// Changes the range of data that a graph should show (increasing)
        /// </summary>
        /// <param name="g">The group that should be affected (GraphCollection)</param>
        public void ShowMoreGraphData(GraphDataGroup g)
        {
            // Change the Range for the group specified.
            bool success = true;
            switch (g)
            {
                case GraphDataGroup.Temp:
                    if (TempRange == _ranges[_ranges.Length - 1])
                        success = false;
                    else
                        for (int i = 0; i < _ranges.Length; i++)
                        {
                            if (TempRange == _ranges[i])
                            {
                                TempRange = _ranges[i + 1];
                                break;
                            }
                        }

                    break;
                case GraphDataGroup.AirFlow:
                    if (AirFlowRange == _ranges[_ranges.Length - 1])
                        success = false;
                    else
                        for (int i = 0; i < _ranges.Length; i++)
                        {
                            if (AirFlowRange == _ranges[i])
                            {
                                AirFlowRange = _ranges[i + 1];
                                break;
                            }
                        }

                    break;
            }

            // If range is not already largest, reload the graph, otherwise show an error
            if (success)
            {
                ReloadGraphData();
            }
            else
            {
                MessageBox.Show("Largest time span to show is: " + _ranges[_ranges.Length - 1].ToString());
            }
        }

        /// <summary>
        /// Changes the range of data that a graph should show (decreasing)
        /// </summary>
        /// <param name="g">The group that should be affected (GraphCollection)</param>
        public void ShowLessGraphData(GraphDataGroup g)
        {
            // Similar to ShowMoreGraphData

            bool success = true;
            switch ( g )
            {
                case GraphDataGroup.Temp:
                    if (TempRange == _ranges[0])
                        success = false;
                    else
                        for (int i = 0; i < _ranges.Length; i++) {
                            if (TempRange == _ranges[i])
                            {
                                TempRange = _ranges[i - 1];
                                break; 
                            }
                        }

                        break;
                case GraphDataGroup.AirFlow:
                        if (AirFlowRange == _ranges[0])
                        success = false;
                    else
                        for (int i = 0; i < _ranges.Length; i++)
                        {
                            if (AirFlowRange == _ranges[i])
                            {
                                AirFlowRange = _ranges[i - 1];
                                break;
                            }
                        }

                    break;
            }

            if (success)
            {
                ReloadGraphData();
            }
            else
            {
                MessageBox.Show("Smallest time span to show is: " + _ranges[0].ToString());
            }
        }

        /// <summary>
        /// Filters the an array of datapoints, limiting the number of datapoints to about <see cref="ESTIMATED_POINTS_TO_SHOW"/> (evenly distributed)
        /// </summary>
        /// <param name="timeSpan">The range for which data should be filtered</param>
        /// <param name="dataPoints">The entire array of datapoints</param>
        /// <returns>Filtered array of datapoints</returns>
        private SerializableKeyValuePair<DateTime, double>[] FilterDataPoints(TimeSpan timeSpan, SerializableKeyValuePair<DateTime, double>[] dataPoints)
        {
            // Can definately be improved (also in respect to performance)!

            // If empty array, ignore
            if (dataPoints == null || dataPoints.Length == 0) return dataPoints;

            // Sort the datapoints on datetime, oldest first out
            Array.Sort(dataPoints, delegate(SerializableKeyValuePair<DateTime, double> d1, SerializableKeyValuePair<DateTime, double> d2) { return d1.Key.CompareTo(d2.Key); });

            // If requested timespan is larger than the range of available data, just use the oldest datapoint as start of range
            if (DateTime.Now.Subtract(timeSpan) < dataPoints[0].Key)
                timeSpan = DateTime.Now.Subtract(dataPoints[0].Key).Subtract(TimeSpan.FromMinutes(1)); // Tiny difference to ensure oldest datapoint is included

            // Minutes between datapoints to get evenly distributed datapoints with the amount of points to show
            int minPerPoint = ((int)timeSpan.TotalMinutes) / ESTIMATED_POINTS_TO_SHOW;

            // Prepare result list
            List<SerializableKeyValuePair<DateTime, double>> l = new List<SerializableKeyValuePair<DateTime, double>>();

            DateTime lastPoint = DateTime.Now; // Last datapoint added to result, irellevant initialization
            bool first = true; // First loop
            bool addedLast = false; // Whether or not the last (newest) datapoint was added
            foreach ( SerializableKeyValuePair<DateTime, double> dataPoint in dataPoints )
            {
                addedLast = false; // Another loop, not yet added this dataPoint

                // Only latest [timeSpan] hours/days...
                if (dataPoint.Key < DateTime.Now.Subtract(timeSpan)) continue;

                if (first)
                {
                    // On first loop, always add the data point
                    first = false;
                    lastPoint = dataPoint.Key;
                    l.Add(dataPoint);
                    addedLast = true; // The datapoint of this loop was added
                }
                else
                {
                    // Get the difference in time from last datapoint added to result
                    TimeSpan diff = dataPoint.Key - lastPoint;

                    // If large enough difference (to get evenly distributed datapoints), add it (allow tiny differences in time)
                    if ( diff.TotalMinutes >= minPerPoint - 1)
                    {
                        if (diff.TotalMinutes != 0 && timeSpan.TotalMinutes != 0) // Avoid possible division by 0
                        {
                            // This block ensures that added times are evenly distributed

                            // How many should have been added since last added datapoint (according to minPerPoint)
                            int howFar = (int)Math.Floor(diff.TotalMinutes / (double)minPerPoint );
                            // Add enough time to get past the time of the new datapoint
                            TimeSpan addedTime = TimeSpan.FromMinutes(minPerPoint);
                            for (int i = 1; i < howFar; i++)
                                addedTime = addedTime.Add(TimeSpan.FromMinutes(minPerPoint));

                            lastPoint = lastPoint.Add(addedTime);
                        }
                        else
                        {
                            lastPoint = dataPoint.Key;
                        }

                        // Add datapoint
                        l.Add(dataPoint);
                        addedLast = true;
                    }
                }
            }

            // Ensure newest data point is always added, just in case
            if (!addedLast) l.Add(dataPoints[dataPoints.Length - 1]);

            // Must return an array
            return l.ToArray();
        }


        #region Helpers
        private void RaisePropertyChanged(string prop)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(prop));
        }
        #endregion
    }
}
